package uk.ac.cam.bt288.Pico2;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.AlgorithmParameters;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import android.app.ListActivity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import biz.source_code.base64Coder.Base64Coder;

public class MainActivity extends ListActivity {
	/** Called when the activity is first created. */

	private static final String TAG = "Pico";

	private static final int EXIT_CMD = -1;
	private static final int AUTHENTICATE = 1;
	private static final int NEWACCOUNT = 2;

	public static final int APPKEY = 0;
	public static final int MACADR = 1;
	public static final int UUID_STR = 2;
	public static final int DB_APPKEY = 1;
	public static final int DB_USERNAME = 2;
	public static final int DB_PUBKEY = 3;
	public static final int DB_PRIVKEY = 4;

	//Intent request codes
	private static final int REQUEST_ENABLE_BT = 3;
	private static final int SCAN_QR_CODE = 0;

	private byte[] iv = null;

	private DBAdapter db = null;
	private String appKey = null;
	private String macAdd = null;
	private UUID my_uuid = null;
	private BluetoothSocket bt = null;
	private InputStream in = null;
	private OutputStream out = null;
	private BluetoothAdapter mBluetoothAdapter;
	private Button auth_btn = null;
	private Button newAcc_btn = null;
	private Button submit = null;
	private EditText mEdit = null;
	private TextView logIn = null;
	private Map <String, KeyPair> accounts = null;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		// Set up the window layout
		setContentView(R.layout.main);

		//instance of database adapter
		db = new DBAdapter(this);

		// Get local Bluetooth adapter
		mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

		// If the adapter is null, then Bluetooth is not supported
		if (mBluetoothAdapter == null) {
			Toast.makeText(this, "Bluetooth is not available", Toast.LENGTH_LONG).show();
			finish();
			return;
		}

		auth_btn = (Button) findViewById(R.id.btn_auth);
		mEdit = (EditText)findViewById(R.id.text_username);
		newAcc_btn = (Button) findViewById(R.id.btn_newAcc);
		submit = (Button) findViewById(R.id.btn_submit);
		logIn = (TextView) findViewById(R.id.logInText);
		
		auth_btn.setVisibility(View.INVISIBLE);
		newAcc_btn.setVisibility(View.INVISIBLE);
		mEdit.setVisibility(View.INVISIBLE);
		submit.setVisibility(View.INVISIBLE);
		logIn.setVisibility(View.INVISIBLE);

		//load database
		db.load();
	}

	@Override
	public void onStart() {
		super.onStart();

		// If BT is not on, request that it be enabled.
		if (!mBluetoothAdapter.isEnabled()) {
			Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
			startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
			// Otherwise, setup the session
		} else {
			setupSession();
		}
	}

	private void setupSession () {
		//Authenticate
		auth_btn.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				mEdit.setVisibility(View.INVISIBLE);
				submit.setVisibility(View.INVISIBLE);
				logIn.setVisibility(View.VISIBLE);
				generateList();
			}
		});
		

		//Create new account
		newAcc_btn.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View arg0) {
				submit.setVisibility(View.VISIBLE);
				mEdit.setVisibility(View.VISIBLE);
			}
		});
		
		//submit new account
		submit.setOnClickListener(new View.OnClickListener() {
			
			@Override
			public void onClick(View arg0) {
				try{
					out.write(NEWACCOUNT);
					out.flush();
				} catch (IOException e) {
					e.printStackTrace();
				}
				newAccount();
				
			}
		});

		//Scan QR Code
		Button scan = (Button) findViewById(R.id.btn_scan);
		scan.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				if (bt != null) {
					try {
					in.close();
					out.close();
					bt.close();
					bt = null;
					} catch (Exception e) {
						Log.d(TAG, e.toString());
					}
				}
				Intent intent = new Intent("com.google.zxing.client.android.SCAN");
				intent.putExtra("SCAN_MODE", "QR_CODE_MODE");
				startActivityForResult(intent, SCAN_QR_CODE);
			}
		});


	}
	
	@Override
	protected void onListItemClick(ListView l, View v, int position, long id) {
		String item = (String) getListAdapter().getItem(position);
		try {
			out.write(AUTHENTICATE);
			out.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}

		authenticate(accounts.get(item));
	}

	@Override
	protected void onDestroy() {
		super.onDestroy();
		db.close();
		if (bt != null) {
			try {
				out.write(EXIT_CMD);
				bt.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	@Override
	public void onActivityResult(int requestCode, int resultCode, Intent intent) {
		if (requestCode == SCAN_QR_CODE) {
			if (resultCode == RESULT_OK) {
				//Successful scan
				processQR(intent.getStringExtra("SCAN_RESULT"));
				//String format = intent.getStringExtra("SCAN_RESULT_FORMAT"); //format of the code

				//Toast.makeText(this, contents, Toast.LENGTH_LONG).show();
			} else if (resultCode == RESULT_CANCELED) {
				Toast.makeText(this, "Scan failed!", Toast.LENGTH_SHORT).show();
			}
		}
		if (requestCode == REQUEST_ENABLE_BT) {
			// When the request to enable Bluetooth returns
			if (resultCode == ListActivity.RESULT_OK) {
				// Bluetooth is now enabled, so set up a chat session
				setupSession();
			} else {
				// User did not enable Bluetooth or an error occurred
				Log.d(TAG, "BT not enabled");
				Toast.makeText(this, "Bluetooth cannot be enabled", Toast.LENGTH_SHORT).show();
				finish();
			}
		}

	}

	public void processQR (String content) {
		String[] contents = content.split(" ");
		if (contents.length != 3) {
			Log.e(TAG, "Not well formed QR Code");
		}
		else {
			appKey = contents[APPKEY];
			macAdd = contents[MACADR];
			my_uuid = UUID.fromString(contents[UUID_STR]);
			Log.d(TAG, macAdd);
			String appName = db.getAppName(appKey);
			Log.d(TAG, appName);
			if (appName == null)
				return;
			Toast.makeText(this, appName, Toast.LENGTH_SHORT).show();
			BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(macAdd);
			try {
				bt = device.createInsecureRfcommSocketToServiceRecord(my_uuid);
				bt.connect();
				in = bt.getInputStream();
				out = bt.getOutputStream();
			} catch (IOException e) {
				e.printStackTrace();
			}

			newAcc_btn.setVisibility(View.VISIBLE);
			auth_btn.setVisibility(View.VISIBLE);

			
		}
	}
	
	void generateList () {
		Cursor c = db.appAccounts(appKey);
		if (c.moveToFirst()) {
			accounts = new HashMap<String, KeyPair>();
			List<String> users = new ArrayList<String>();
			do {
				try {
					String username = c.getString(DB_USERNAME);
					PublicKey pubKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(Base64Coder.decode(c.getString(DB_PUBKEY))));
					PrivateKey privKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(Base64Coder.decode(c.getString(DB_PRIVKEY).toCharArray())));
					KeyPair kp = new KeyPair (pubKey, privKey);
					users.add(username);
					accounts.put(username, kp);
				} catch (Exception e) {
					e.printStackTrace();
				}
			} while (c.moveToNext());
			ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
					android.R.layout.simple_list_item_1, users);
			setListAdapter(adapter);

		}
	}

	public void newAccount () {
		byte[] servMessage = new byte[1024];
		int msgLength = -1;

		KeyPair newKey = Keys.GenerateKeyPair();

		try {
			//Construct app's public key from QR code 
			PublicKey pubKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(Base64Coder.decode(appKey)));
			Log.d(TAG, "servKey: "+new String(Base64Coder.encode(pubKey.getEncoded())));

			//Send temp public key to the app, and generate random string
			out.write(newKey.getPublic().getEncoded());
			String randStr = Keys.nextSessionId();
			Log.d(TAG, "Rand str: "+randStr);

			//encrypt random string with public key of app, and send to app
			byte[] sendData = Keys.encrypt(randStr, pubKey);
			Log.d(TAG, "SendData: "+new String(Base64Coder.encode(sendData)));
			out.write(sendData);
			out.flush();

			//read encrypted random string from server, decrypt with private key, finished authenticating app
			msgLength = in.read(servMessage);
			Log.d(TAG, "msgLength: "+msgLength);
			String rcvStr = Keys.decrypt(Arrays.copyOfRange(servMessage, 0, msgLength), newKey.getPrivate());
			Log.d(TAG, "received string: "+rcvStr);
			if (!rcvStr.equals(randStr)) {
				Log.w(TAG, "Application authentication failed");
				return;
			}

			String user = mEdit.getText().toString();
			byte[] username = user.getBytes();
			out.write(username);
			out.flush();
			int rtnCode = in.read();
			if (rtnCode == 0) {
				db.insertEntry(appKey, user, new String(Base64Coder.encode(newKey.getPublic().getEncoded())), 
						new String(Base64Coder.encode(newKey.getPrivate().getEncoded())));
				Toast.makeText(this, "User has been added", Toast.LENGTH_SHORT).show();
			} else {
				Toast.makeText(this, "User exists", Toast.LENGTH_SHORT).show();
			}

		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void authenticate (KeyPair k) {
		
		PrivateKey priKey = k.getPrivate();

		//Log.d(Tag, new String(Base64Coder.encode(priKey.getEncoded())));
		//byte[] servKey = new byte[1024];
		byte[] servMessage = new byte[1024];
		//int servKeyLength = -1;
		int msgLength = -1;

		KeyPair temp = Keys.GenerateKeyPair(); //generate temporary key pair for privacy

		try {
			//Construct app's public key from QR code 
			PublicKey pubKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(Base64Coder.decode(appKey)));
			Log.d(TAG, "servKey: "+new String(Base64Coder.encode(pubKey.getEncoded())));

			//Send temp public key to the app, and generate random string
			out.write(temp.getPublic().getEncoded());
			String randStr = Keys.nextSessionId();
			Log.d(TAG, "Rand str: "+randStr);

			//encrypt random string with public key of app, and send to app
			byte[] sendData = Keys.encrypt(randStr, pubKey);
			Log.d(TAG, "SendData: "+new String(Base64Coder.encode(sendData)));
			out.write(sendData);
			out.flush();

			//read encrypted random string from server, decrypt with temp private key, finished authenticating app
			msgLength = in.read(servMessage);
			Log.d(TAG, "msgLength: "+msgLength);
			String rcvStr = Keys.decrypt(Arrays.copyOfRange(servMessage, 0, msgLength), temp.getPrivate());
			Log.d(TAG, "received string: "+rcvStr);
			if (!rcvStr.equals(randStr)) {
				Log.w(TAG, "Application authentication failed");
				return;
			}

			//Send real public key to app
			out.write(k.getPublic().getEncoded());
			//Receive and decrypt random string from server
			msgLength = in.read(servMessage);
			rcvStr = Keys.decrypt(Arrays.copyOfRange(servMessage, 0, msgLength), priKey);
			//Send back random string to server, authentication complete.
			Log.d(TAG, "Strlen: "+msgLength+"   rcvStr: "+rcvStr);
			out.write(Keys.encrypt(rcvStr, pubKey));

		} catch (Exception e) {
			e.printStackTrace();
			return;
		}

	}

	public void DisplayEntry (Cursor c){
		Toast.makeText(this, "App key: " + c.getString(1)+"\n"+"Username: "+c.getString(2)+"\n", Toast.LENGTH_LONG).show();
		Log.d(TAG, "PubKey: "+c.getString(3));
		Log.d(TAG, "PrivKey: " + c.getString(4));
	}

	public String AESEncrypt (String data, String key) {
		try {
			SecretKeySpec k = new SecretKeySpec(key.getBytes(), "AES");

			Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
			c.init(Cipher.ENCRYPT_MODE, k);

			AlgorithmParameters params = c.getParameters();
			iv = params.getParameterSpec(IvParameterSpec.class).getIV();

			return new String(c.doFinal(data.getBytes()));
		}
		catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	public String AESDecrypt (String data, String key) {
		try {
			Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
			SecretKeySpec k =
					new SecretKeySpec(key.getBytes(), "AES");
			c.init(Cipher.DECRYPT_MODE, k, new IvParameterSpec(iv));
			return new String(c.doFinal(data.getBytes()));
		}
		catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
}